import 'package:common_utils/common_utils.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/services.dart';
import 'dart:math' as math;

class NumberLimitingFormatter extends TextInputFormatter {
  /// Creates a formatter that prevents the insertion of more characters than a
  /// limit.
  ///
  /// The [maxLength] must be null, -1 or greater than zero. If it is null or -1
  /// then no limit is enforced.
  NumberLimitingFormatter({this.maxNumber, this.minNumber, this.allowDecimals = false});

  /// The limit on the number of characters (i.e. Unicode scalar values) this formatter
  /// will allow.
  ///
  /// The value must be null or greater than zero. If it is null, then no limit
  /// is enforced.
  ///
  /// This formatter does not currently count Unicode grapheme clusters (i.e.
  /// characters visible to the user), it counts Unicode scalar values, which leaves
  /// out a number of useful possible characters (like many emoji and composed
  /// characters), so this will be inaccurate in the presence of those
  /// characters. If you expect to encounter these kinds of characters, be
  /// generous in the maxLength used.
  ///
  /// For instance, the character "ö" can be represented as '\u{006F}\u{0308}',
  /// which is the letter "o" followed by a composed diaeresis "¨", or it can
  /// be represented as '\u{00F6}', which is the Unicode scalar value "LATIN
  /// SMALL LETTER O WITH DIAERESIS". In the first case, the text field will
  /// count two characters, and the second case will be counted as one
  /// character, even though the user can see no difference in the input.
  ///
  /// Similarly, some emoji are represented by multiple scalar values. The
  /// Unicode "THUMBS UP SIGN + MEDIUM SKIN TONE MODIFIER", "👍🏽", should be
  /// counted as a single character, but because it is a combination of two
  /// Unicode scalar values, '\u{1F44D}\u{1F3FD}', it is counted as two
  /// characters.
  ///
  /// ### Composing text behaviors
  ///
  /// There is no guarantee for the final value before the composing ends.
  /// So while the value is composing, the constraint of [maxLength] will be
  /// temporary lifted until the composing ends.
  ///
  /// In addition, if the current value already reached the [maxLength],
  /// composing is not allowed.
  final num? maxNumber;
  final num? minNumber;
  final bool allowDecimals;

  /// Truncate the given TextEditingValue to maxLength characters.
  ///
  /// See also:
  ///  * [Dart's characters package](https://pub.dev/packages/characters).
  ///  * [Dart's documenetation on runes and grapheme clusters](https://dart.dev/guides/language/language-tour#runes-and-grapheme-clusters).
  static TextEditingValue truncate(TextEditingValue value, int maxLength) {
    final CharacterRange iterator = CharacterRange(value.text);
    if (value.text.characters.length > maxLength) {
      iterator.expandNext(maxLength);
    }
    final String truncated = iterator.current;
    return TextEditingValue(
      text: truncated,
      selection: value.selection.copyWith(
        baseOffset: math.min(value.selection.start, truncated.length),
        extentOffset: math.min(value.selection.end, truncated.length),
      ),
      composing: TextRange.empty,
    );
  }

  @override
  TextEditingValue formatEditUpdate(
    TextEditingValue oldValue,
    TextEditingValue newValue,
  ) {
    try {
      if (ObjectUtil.isEmpty(newValue.text)) {
        return newValue;
      }
      if (minNumber == null || maxNumber == null) {
        return oldValue;
      }
      if (allowDecimals) {
        double number = double.parse(newValue.text);
        if (number >= minNumber! && number <= maxNumber!) {
          return newValue;
        }
        if (number == 0) {
          return TextEditingValue();
        }
      } else {
        int number = int.parse(newValue.text);
        if (number >= minNumber! && number <= maxNumber!) {
          return newValue;
        }
        if (number == 0) {
          return TextEditingValue();
        }
      }
    } catch (e) {
      return oldValue;
    }

    return oldValue;
  }
}
